/*
 * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.dtv.lwuit;

import com.sun.dtv.lwuit.events.ActionEvent;
import com.sun.dtv.lwuit.events.ActionListener;
import com.sun.dtv.lwuit.geom.Dimension;
import com.sun.dtv.lwuit.geom.Rectangle;
import com.sun.dtv.lwuit.plaf.Style;
import com.sun.dtv.lwuit.plaf.UIManager;
import java.util.Vector;

/**
 * An optionally multi-line editable region that can display text and allow a user to edit it.
 * Depending on the platform editing might occur in a new screen. Notice that when creating
 * a text area with one row it will act as a text field and never grow beyond that, however 
 * when assigning a greater number of rows the text area becomes multi-line with a minimum
 * number of visible rows, the text area will grow based on its content.
 *
 * @author Chen Fishbein
 */
public class TextArea extends Component {
    private static int defaultMaxSize = 124;
    private static boolean autoDegradeMaxSize = false;
    private static boolean hadSuccessfulEdit = false;
    
    /**
     * Allows any type of input into a text field, if a constraint is not supported
     * by an underlying implementation this will be the default.
     */
    public static final int ANY = 0;

    /**
     * The user is allowed to enter an e-mail address.
     */
    public static final int EMAILADDR = 1;

    /**
     * The user is allowed to enter only an integer value.
     */
    public static final int NUMERIC = 2;

    /**
     * The user is allowed to enter a phone number.
     */
    public static final int PHONENUMBER = 3;

    /**
     * The user is allowed to enter a URL.
     */
    public static final int URL = 4;

    /**
     * The user is allowed to enter numeric values with optional decimal 
     * fractions, for example "-123", "0.123", or ".5".
     */
    public static final int DECIMAL = 5;
    
    /**
     * Indicates that the text entered is confidential data that should be 
     * obscured whenever possible.
     */
    public static final int PASSWORD = 0x10000;

    /**
     *  Indicates that editing is currently disallowed.
     */
    public static final int UNEDITABLE = 0x20000;

    /**
     * Indicates that the text entered is sensitive data that the 
     * implementation must never store into a dictionary or table for use 
     * in predictive, auto-completing, or other accelerated input schemes.
     */
    public static final int SENSITIVE = 0x40000;

    /**
     * Indicates that the text entered does not consist of words that are 
     * likely to be found in dictionaries typically used by predictive input 
     * schemes.
     */
    public static final int NON_PREDICTIVE= 0x80000;

    /**
     * This flag is a hint to the implementation that during text editing, 
     * the initial letter of each word should be capitalized.
     */
    public static final int INITIAL_CAPS_WORD = 0x100000;

    /**
     * This flag is a hint to the implementation that during text editing, 
     * the initial letter of each sentence should be capitalized.
     */
    public static final int INITIAL_CAPS_SENTENCE = 0x200000;
    //private int modifierFlag = 0x00000;
             
    /**
     * Input constraint which should be one of CONSTRAINT_ANY, CONSTRAINT_NUMERIC,
     * CONSTRAINT_PHONENUMBER, CONSTRAINT_URL or CONSTRAINT_EMAIL
     */
    private int constraint = ANY;
    
    private  String text="";
    
    private  boolean editable = true;
    
    private int maxSize = defaultMaxSize ; //maximum size (number of characters) that can be stored in this TextField.
    
    private int rows = 1;
    
    private int columns = 1;
    
    // problematic  maxSize = 20; //maximum size (number of characters) that can be stored in this TextField.
    
    private static String id = "TextArea";
    
    private Vector rowStrings;
    private int widthForRowCalculations = -1;

    private int rowsGap = 2;

    private boolean triggerClose;

    private Vector actionListeners = null;
    
    /**
     * Indicates that the text area should "grow" in height based on the content beyond the
     * limits indicate by the rows variable
     */
    private boolean growByContent = true;
    
    /**
     * Creates an area with the given rows and columns
     * 
     * @param rows the number of rows
     * @param columns - the number of columns
     */
    public TextArea(int rows, int columns){
        this("", defaultMaxSize, rows, columns, ANY);
    }

    /**
     * Creates an area with the given rows, columns and constrint 
     * 
     * @param rows the number of rows
     * @param columns - the number of columns
     * @param constraint one of ANY, EMAILADDR, NUMERIC, PHONENUMBER, URL, DECIMAL
     * it can be bitwised or'd with one of PASSWORD, UNEDITABLE, SENSITIVE, NON_PREDICTIVE,
     * INITIAL_CAPS_SENTENCE, INITIAL_CAPS_WORD. E.g. ANY | PASSWORD.
     */
    public TextArea(int rows, int columns, int constraint){
        this("", defaultMaxSize, rows, columns, constraint);
    }
    
    /**
     * Creates an area with the given text, rows and columns
     * 
     * @param text the text to be displayed; if text is null, the empty 
     * string "" will be displayed
     */
    public TextArea(String text, int rows, int columns){
        this(text,defaultMaxSize, rows, columns, ANY); //String , maxSize, constraints= 0 (ANY)
    }

    /**
     * Creates an area with the given text, rows, columns and constrint 
     * 
     * @param text the text to be displayed; if text is null, the empty 
     * string "" will be displayed
     * @param rows the number of rows
     * @param columns - the number of columns
     * @param constraint one of ANY, EMAILADDR, NUMERIC, PHONENUMBER, URL, DECIMAL
     * it can be bitwised or'd with one of PASSWORD, UNEDITABLE, SENSITIVE, NON_PREDICTIVE,
     * INITIAL_CAPS_SENTENCE, INITIAL_CAPS_WORD. E.g. ANY | PASSWORD.
     */
    public TextArea(String text, int rows, int columns, int constraint){
        this(text,defaultMaxSize, rows, columns, constraint); 
    }

    /**
     * Creates an area with the given text and maximum size, this constructor
     * will create a single line text area similar to a text field! 
     * 
     * @param text the text to be displayed; if text is null, the empty 
     * string "" will be displayed
     * @param maxSize text area maximum size
     */
    public TextArea(String text, int maxSize){
        this(text,maxSize, 1, 1, ANY); //String , maxSize, constraints= 0 (ANY)
    }
    
    /**
     * Creates an area with the given text, this constructor
     * will create a single line text area similar to a text field! 
     * 
     * @param text the text to be displayed; if text is null, the empty 
     * string "" will be displayed
     */
    public TextArea(String text) {
        this(text, Math.max(defaultMaxSize, text.length()), 1, 1, ANY); //String , maxSize, constraints= 0 (ANY)
    }

    /**
     * Creates an empty text area, this constructor
     * will create a single line text area similar to a text field! 
     */
    public TextArea() {
        this("");
    }
    
    /**
     * Creates an area with the given text, maximum size, rows, columns and constrint 
     * 
     * @param text the text to be displayed; if text is null, the empty 
     * string "" will be displayed
     * @param maxSize text area maximum size
     * @param rows the number of rows
     * @param columns - the number of columns
     * @param constraint one of ANY, EMAILADDR, NUMERIC, PHONENUMBER, URL, DECIMAL
     * it can be bitwised or'd with one of PASSWORD, UNEDITABLE, SENSITIVE, NON_PREDICTIVE,
     * INITIAL_CAPS_SENTENCE, INITIAL_CAPS_WORD. E.g. ANY | PASSWORD.
     */
    private TextArea(String text, int maxSize, int rows, int columns, int constraint){
        this.maxSize = maxSize;
        setText(text);
        setConstraint(constraint);
        this.rows = rows;
        this.columns = columns;
        setSmoothScrolling(false);
    }

    /**
     * Sets the constraint 
     * 
     * @param constraint one of ANY, EMAILADDR, NUMERIC, PHONENUMBER, URL, DECIMAL
     * it can be bitwised or'd with one of PASSWORD, UNEDITABLE, SENSITIVE, NON_PREDICTIVE,
     * INITIAL_CAPS_SENTENCE, INITIAL_CAPS_WORD. E.g. ANY | PASSWORD.
     */
    public void setConstraint(int constraint) {
        this.constraint = constraint;
    }


    /**
     * Returns the editing constraint value
     * 
     * @return the editing constraint value
     * @see #setConstraint
     */
    public int getConstraint() {
        return constraint;
    }

    /**
     * Sets the text within this text area
     * 
     * @param t new value for the text area
     */
    public void setText(String t) {
        this.text = (t != null) ? t : "";
        setShouldCalcPreferredSize(true);
        if(maxSize < text.length()) {
            maxSize = text.length() + 1;
        }
        // special case to make the text field really fast...
        rowStrings=null; //zero the vector inorder to initialize it on the next paint
        repaint();
    }

    /**
     * Returns the text in the text area
     * 
     * @return the text in the text area
     */
    public String getText() {
        return text;
    }
    
    /**
     * Returns true if this area is editable
     * 
     * @return true if this area is editable
     */
    public boolean isEditable() {
        return editable;
    }

    /**
     * Sets this text area to be editable or readonly
     * 
     * @param b true is text are is editable; otherwise false
     */
    public void setEditable(boolean b) {
        editable = b;
    }

    /**
     * Returns the maximum size for the text area
     * 
     * @return the maximum size for the text area
     */
    public int getMaxSize() {
        return maxSize;
    }

    /**
     * Sets the maximum size of the text area
     * 
     * @param maxSize the maximum size of the text area
     */
    public void setMaxSize(int maxSize) {
        this.maxSize = maxSize;
    }
    
    /**
     * @inheritDoc
     */
    public void keyPressed(int keyCode) {
        super.keyPressed(keyCode);
        
        int action = com.sun.dtv.lwuit.Display.getInstance().getGameAction(keyCode);

        // this works around a bug where fire is also a softkey on devices such as newer Nokia
        // series 40's (e.g. the Nokia emulator). It closes its native text box on fire then
        // as a result of a Nokia bug we get the key released of that closing and assume the
        // users wants to edit the text... When means the only way to exit the native text box
        // is via the cancel option (after pressing OK once).
        triggerClose = action == Display.GAME_FIRE;

        //scroll the TextArea
        Rectangle rect = new Rectangle(getScrollX(), getScrollY(), getWidth(), getHeight());
        Font textFont = getStyle().getFont();
        if(action == Display.GAME_DOWN){
            if((getScrollY() + getHeight()) <(rowsGap + getStyle().getFont().getHeight()) * getLines()) {
                rect.setY(rect.getY() + textFont.getHeight() + rowsGap);
                scrollRectToVisible(rect, this);
            } else {
                setHandlesInput(false);
            }
        }else if(action == Display.GAME_UP){
            if(getScrollY() > 0) {
                rect.setY(Math.max(0, rect.getY() - textFont.getHeight() - rowsGap));
                scrollRectToVisible(rect, this);
            } else {
                setHandlesInput(false);
            }
        }
    }
    
    
    /**
     * @inheritDoc
     */
    protected void fireClicked() {
        onClick();
    }
    
    /**
     * @inheritDoc
     */
    protected boolean isSelectableInteraction() {
        return editable;
    }

    /**
     * @inheritDoc
     */
    public void keyReleased(int keyCode) {
        int action = com.sun.dtv.lwuit.Display.getInstance().getGameAction(keyCode);
        if(isEditable()){
            // this works around a bug where fire is also a softkey on devices such as newer Nokia
            // series 40's
            if (triggerClose && action == Display.GAME_FIRE) {
                triggerClose = false;
                onClick();
                return;
            }
        }
    }
    
    /**
     * @inheritDoc
     */
    public boolean isScrollableY() {
        return (rowsGap + getStyle().getFont().getHeight()) * getLines() > getHeight();
    }

    /**
     * @inheritDoc
     */
    protected void paintScrollbarY(Graphics g) {
        int prefH = (rowsGap + getStyle().getFont().getHeight()) * getLines();
        float offset = ((float) getScrollY()) / ((float) prefH);
        float block = ((float) getHeight()) / ((float) prefH);
        UIManager.getInstance().getLookAndFeel().drawVerticalScroll(g, this, offset, block);
    }
    
    void onClick(){
        if(isEditable()) {
            editString();
        }
    }
        
    void editString() {
        if(autoDegradeMaxSize && (!hadSuccessfulEdit) && (maxSize > 1024)) {
            try {
                Display.getInstance().editString(this, getMaxSize(), getConstraint(), getText());
            } catch(IllegalArgumentException err) {
                maxSize -= 1024;
                setDefaultMaxSize(maxSize);
                editString();
            }
        } else {
            Display.getInstance().editString(this, getMaxSize(), getConstraint(), getText());
        }
    }
    
    /**
     * @inheritDoc
     */
    public void pointerReleased(int x, int y) {
        super.pointerReleased(x, y);
        if(isEditable()){
            onClick();
        }
    }

    /**
     * @inheritDoc
     */
    void focusGainedInternal() {
        super.focusGainedInternal();
        setHandlesInput(isScrollableY());
    }

    /**
     * @inheritDoc
     */
    void focusLostInternal() {
        super.focusLostInternal();
        setHandlesInput(false);
    }
    
    /**
     * Returns the number of columns in the text area
     * 
     * @return the number of columns in the text area
     */
    public int getColumns() {
        return columns;
    }
    
    /**
     * Returns the number of rows in the text area
     * 
     * @return the number of rows in the text area
     */
    public int getRows() {
        return rows;
    }
    
    /**
     * Sets the number of columns in the text area
     * 
     * @param columns number of columns
     */
    public void setColumns(int columns) {
        setShouldCalcPreferredSize(true);
        this.columns = columns;
    }
    
    /**
     * Sets the number of rows in the text area
     * 
     * @param rows number of rows
     */
    public void setRows(int rows) {
        setShouldCalcPreferredSize(true);
        this.rows = rows;
    }
    
    /**
     * @inheritDoc
     */
    protected String getUIID() {
        return id;
    }

    void initComponentImpl() {
        super.initComponentImpl();
        getRowStrings();
    }
    
    private Vector getRowStrings() {
        if(rowStrings == null || widthForRowCalculations != getWidth()){
            initRowString();
            setShouldCalcPreferredSize(true);
            //setPreferredH(getStyle().getPadding(TOP) + getStyle().getPadding(BOTTOM) + (getStyle().getFont().getHeight() + rowsGap)*rowStrings.size());
        }
        return rowStrings;
    }
    
    
    /**
     * Returns the number of text lines in the TextArea
     * 
     * @return the number of text lines in the TextArea
     */
    public int getLines(){
        int retVal;
        Vector v = getRowStrings();
        v.trimToSize();
        retVal = v.size();
        if(growByContent){
            rows = Math.max(rows, retVal);
        }
        return retVal;
    }
    
    /**
     * Returns the text in the given row of the text box
     * 
     * @param line the line number in the text box
     */
    public String getTextAt(int line){
        Vector rowsV = getRowStrings();
        return (String)rowsV.elementAt(line);
    }
    
    private int indexOf(char[] t, char c, int offset) {
        for(int iter = offset ; iter < t.length ; iter++) {
            if(t[iter] == c) {
                return iter;
            }
        }
        return -1;
    }
    
    private boolean fastCharWidthCheck(char[] chrs, int off, int length, int width, int charWidth, Font f) {
        if(length * charWidth < width) {
            return true;
        }
        return f.charsWidth(chrs, off, length) < width;
    }
    
    private void initRowString() {
        rowStrings= new Vector();
        widthForRowCalculations = getWidth();
        
        // single line text area is essentially a text field
        if(rows == 1) {
            rowStrings.addElement(getText());
            return;
        }
        if(text == null || text.equals("")){
            return;
        }
        char[] text = getText().toCharArray();
        
        Style style = getStyle();
        Font font = style.getFont();
        int charWidth = font.charWidth('W');
        int textAreaWidth = getWidth()- style.getPadding(RIGHT) - style.getPadding(LEFT)- 
            style.getMargin(RIGHT) - style.getMargin(LEFT); //(border(=gap) + gap )*2
        if(textAreaWidth <= 0) {
            textAreaWidth = Math.min(10, columns) * charWidth;
        }
        int minCharactersInRow = Math.max(1, textAreaWidth / charWidth);
        int rowIndex=0;
        int from=0;
        int to=from+minCharactersInRow;
        int textLength=text.length;
        String rowText;
        int i,spaceIndex;
        // if there is any possibility of a scrollbar we need to reduce the textArea
        // width to accomodate it
        if(textLength / minCharactersInRow > Math.max(2, rows)) {
            textAreaWidth -= UIManager.getInstance().getLookAndFeel().getVerticalScrollWidth();
        }
        
        /*
        iteration over the string using indexes, from - the begining of the row , to - end of a row
        for each row we will try to search for a "space" character at the end of the row ( row is text area available width)
        indorder to improve the efficiency we do not search an entire row but we start from minCharactersInRow which indicates
        what is the minimum amount of characters that can feet in the text area width.
        if we dont find we will go backwards and search for the first space available,
        if there is no space in the entire row we will cut the line inorder to fit in.
         */
        if(textLength<=minCharactersInRow){
            rowStrings.addElement(new String(text));
        } else{
            while(to<textLength) {
                if(to>textLength){
                    to=textLength;
                }
                
                spaceIndex=-1;
                rowText="";
                int maxLength = to;
                
                // search for "space" character at close as possible to the end of the row
                for( i=to; i < textLength && fastCharWidthCheck(text, from, i - from, textAreaWidth, charWidth, font)  ; i++){
                    char c = text[i];
                    if(c == ' ' || c == '\n' || c == '\t') {
                        spaceIndex=i;
                    }
                    maxLength++;
                }
                
                // if we got to the end of the text use the entire row,
                // also if space is next character (in the next row) we can cut the line
                if(i == textLength || text[i] == ' ' || text[i] == '\n') {
                    spaceIndex=i;
                }
                
                // if we found space in the limit width of the row (searched only from minCharactersInRow)
                if(spaceIndex!=-1){
                    // make sure that if we have a newline character before the end of the line we should
                    // break there instead
                    int newLine = indexOf(text, '\n', from + 1);
                    if(newLine > -1 && newLine < spaceIndex) {
                        spaceIndex = newLine;
                    }
                    
                    rowText = new String(text, from, spaceIndex - from);
                    from=spaceIndex+1;
                    
                } // if there is no space from minCharactersInRow to limit need to search backwards
                else{
                    for( i=to; spaceIndex==-1 && i>=from ; i--){
                        char chr = text[i];
                        if(chr == ' ' || chr == '\n' || chr == '\t') {
                            spaceIndex=i;
                            rowText = new String(text, from, spaceIndex - from);
                            from=spaceIndex+1;
                        }
                        
                    }
                    if(spaceIndex==-1) {
                        // from = to + 1;
                        spaceIndex = maxLength;
                        rowText = new String(text, from, spaceIndex - from);
                        from = spaceIndex;
                    }
                }
                
                rowStrings.addElement(rowText); 
                //adding minCharactersInRow doesn't work if what is left is less
                //then minCharactersInRow
                to=from;//+minCharactersInRow;
                rowIndex++;
                
            }
        }
    }
    
    /**
     * Gets the num of pixels gap between the rows
     * 
     * @return the gap between rows in pixels
     */
    public int getRowsGap() {
        return rowsGap;
    }

    /**
     * The gap in pixels between rows
     * 
     * @param rowsGap num of pixels to gap between rows
     */
    public void setRowsGap(int rowsGap) {
        this.rowsGap = rowsGap;
    }
    
    /**
     * @inheritDoc
     */
    public void paint(Graphics g) {
        UIManager.getInstance().getLookAndFeel().drawTextArea(g, this);
    }
    
    /**
     * @inheritDoc
     */
    protected Dimension calcPreferredSize(){
        return UIManager.getInstance().getLookAndFeel().getTextAreaPreferredSize(this);
    }
        
    /**
     * Add an action listener which is invoked when the text area was modified not during
     * modification. A text <b>field</b> might never fire an action event if it is edited
     * in place and the user never leaves the text field!
     * 
     * @param a actionListener
     */
    public void addActionListener(ActionListener a) {
        if(actionListeners == null) {
            actionListeners = new Vector();
        }
        if(!actionListeners.contains(a)) {
            actionListeners.addElement(a);
        }
    }

    /**
     * Removes an action listener
     * 
     * @param a actionListener
     */
    public void removeActionListener(ActionListener a) {
        if(actionListeners == null) {
            actionListeners = new Vector();
        }
        actionListeners.removeElement(a);
    }
    
    /**
     * Notifies listeners of a change to the text area
     */
    void fireActionEvent() {
        if(actionListeners != null) {
            ActionEvent evt = new ActionEvent(this);
            for(int iter = 0 ; iter < actionListeners.size() ; iter++) {
                ActionListener a = (ActionListener)actionListeners.elementAt(iter);
                a.actionPerformed(evt);
            }
        }
    }
    
    /**
     * @inheritDoc
     */
    void onEditComplete(String text) {
        setText(text);
    }
    
    /**
     * Sets the default limit for the native text box size
     * 
     * @param value default value for the size of the native text box
     */
    public static void setDefaultMaxSize(int value) {
        defaultMaxSize = value;
    }

    /**
     * Indicates that the text area should "grow" in height based on the content beyond the
     * limits indicate by the rows variable
     * 
     * @return true if the text component should grow and false otherwise
     */
    public boolean isGrowByContent() {
        return growByContent;
    }

    /**
     * Indicates that the text area should "grow" in height based on the content beyond the
     * limits indicate by the rows variable
     * 
     * @param growByContent true if the text component should grow and false otherwise
     */
    public void setGrowByContent(boolean growByContent) {
        this.growByContent = growByContent;
    }
    
    /**
     * Indicates whether a high value for default maxSize will be reduced to a lower
     * value if the underlying platform throws an exception.
     */
    public static void setAutoDegradeMaxSize(boolean value) {
        autoDegradeMaxSize = value;
    }

    /**
     * Indicates whether a high value for default maxSize will be reduced to a lower
     * value if the underlying platform throws an exception.
     */
    public static boolean isAutoDegradeMaxSize() {
        return autoDegradeMaxSize;
    }
}
